// ignore_for_file: non_constant_identifier_names

import 'dart:convert';
import 'dart:io';

import 'package:dart_bean/feature/json_field.dart';
import 'package:dart_bean/setting.dart';
import 'package:dart_bean/util/ext.dart';

Set<JsonToBean> beans = {};
Set<JsonItem> jsonItems = {};

void jsonToBean(String? jsonString, File output, String beanName) {
  if (jsonString == null) {
    throw r'jsonToBean: need copy json string to Clipboard first';
  }
  final json = validateJson(jsonString);
  if (json == null) {
    throw r'jsonToBean: Clipboard json format error, please check it';
  }
  jsonItems.clear();
  beans.clear();
  JsonToBean bean = JsonToBean(r'$');

  beans.add(bean);
  startWithBeanMap(r'$', json);
  // for (var i in jsonItems) {
  //   print(i);
  // }
  parseBean(jsonItems);
  genDartBean(beans, beanName, output);
}

dynamic validateJson(String jsonString) {
  try {
    return jsonDecode(jsonString);
  } catch (e) {
    return null;
  }
}

void startWithBeanMap(String beanName, dynamic json) {
  if (json is Map) {
    json.forEach((k, v) {
      jsonPath(beanName, k, v);
    });
  } else if (json is List) {
    for (var v in json) {
      startWithBeanMap(beanName, v);
    }
  }
}

void jsonPath(String path, String key, dynamic value, [String parentType = '']) {
  if (value is Map) {
    jsonItems.add(JsonItem('$path/$key', '$parentType$path/$key'));
    value.forEach((k, v) {
      jsonPath('$path/$key', k, v, parentType);
    });
  } else if (value is List) {
    for (var v in value) {
      jsonPath(path, '$key>List', v, parentType);
    }
  } else {
    jsonItems.add(JsonItem('$path/$key', value.runtimeType.toString()));
  }
}

class JsonItem {
  final String type;
  late String className, fieldName, fieldType;
  late bool isList;
  late final String typeName = type.split('/').map((e) => e.upperFirst()).join();
  late final List<PathNode> nodes;
  late final String path = nodes.map((e) => e.name).join('/');
  late final String annotation;

  JsonItem(String path, this.type) {
    nodes = PathNode.parse(path);
    init();
  }

  @override
  bool operator ==(Object other) {
    return other is JsonItem && other.hashCode == hashCode;
  }

  void init() {
    fieldName = nodes.last.name;
    final startChar = fieldName.codeUnitAt(0);
    if (startChar >= 'A'.codeUnitAt(0) && startChar <= 'Z'.codeUnitAt(0)) {
      annotation = "@${JsonFieldFeature.annotationClassName}(name:'$fieldName')";
      fieldName = fieldName.lowerFirst();
    } else {
      annotation = '';
    }
    isList = nodes.last.type == 'List';
    final classNameNodes = nodes.sublist(0, nodes.length - 1);
    final typeNodes = PathNode.parse(type);
    if (flattenBean) {
      className = classNameNodes.last.name.upperFirst();
      fieldType = typeNodes.last.name.upperFirst();
    } else {
      className = classNameNodes.map((e) => e.name.upperFirst()).join();
      fieldType = typeNodes.map((e) => e.name.upperFirst()).join();
    }
    switch (fieldType) {
      case 'Int':
        fieldType = (int).toString();
        break;
      case 'Bool':
        fieldType = (bool).toString();
        break;
      case 'Double':
        fieldType = (double).toString();
        break;
    }
  }

  @override
  int get hashCode => toString().hashCode;

  @override
  String toString() {
    return '${nodes.map((e) => e.info).join('/')}:$type';
  }

  late final bool nullable = !isList && type == 'Null';
  late final bool subNullable = isList && type == 'Null';
}

class PathNode {
  final String info;
  late final String name;
  late final String type;

  PathNode(this.info) {
    final nt = info.split('>');
    name = nt[0];
    type = nt.length == 2 ? nt[1] : '';
  }

  static List<PathNode> parse(String info) {
    return info.split('/').map((e) => PathNode(e)).toList();
  }
}

void parseBean(Set<JsonItem> list) {
  for (var i in list) {
    var className = i.className;
    final bean = beans.firstOrNull((b) => b.name == className) ?? JsonToBean(className);
    beans.add(bean);
    String fieldType = getFieldType(i.path);
    bean.fields.add(BeanField(i.fieldName, fieldType, i.annotation));
  }
}

String getFieldType(String path) {
  var results = jsonItems.where((e) => e.path == path);
  bool nullable = results.any((e) => e.nullable);
  bool subNullable = results.any((e) => e.subNullable);
  bool isList = results.any((e) => e.isList);

  final NULL = nullable ? '?' : '';
  final sNULL = subNullable ? '?' : '';

  results = jsonItems.where((e) => e.path == path && !e.type.contains('Null'));

  if (results.isEmpty) {
    return isList ? 'List<dynamic>?' : 'dynamic';
  } else {
    final f = results.first;
    if (results.length == 1) {
      if (isList) {
        return 'List<${f.fieldType}$sNULL>$NULL';
      } else {
        return '${f.fieldType}$NULL';
      }
    } else {
      if (isList) {
        return 'List<dynamic>$NULL';
      } else {
        return 'dynamic';
      }
    }
  }
}

void genDartBean(Set<JsonToBean> beans, String rootName, File output) {
  final sb = StringBuffer();
  for (var bean in beans) {
    sb.writeln('@JsonSerializable()class ${bean.name}{');
    for (var f in bean.fields) {
      sb.writeln(f.annotation);
      sb.writeln('${f.type.endsWith('?') ? '' : 'late '}${f.type} ${f.name};');
    }
    sb.writeln('}');
  }
  var code = sb.toString().replaceAll(r'$', rootName);
  code = safeDartFormatter(code);
  ensureParent(output);
  output.writeAsStringSync(code);
}

class JsonToBean {
  final String name;

  JsonToBean(this.name);

  final Set<BeanField> fields = {};

  @override
  int get hashCode => name.hashCode;

  @override
  bool operator ==(Object other) {
    return other is JsonToBean && other.hashCode == hashCode;
  }
}

class BeanField {
  final String annotation;
  final String name;

  final String type;

  BeanField(this.name, this.type, this.annotation);

  @override
  int get hashCode => '$name:$type'.hashCode;

  @override
  bool operator ==(Object other) {
    return other is BeanField && other.hashCode == hashCode;
  }
}
